#include "vg/stroke.h"
#include "shader.h"
#include "shape.h"
#include "shared.h"
#include "draw.h"

using namespace std;
using namespace anip;

const string vertGLSL = R"(
#version 410
layout (location = 0) in vec2 vPos;
layout (location = 1) in vec2 vNorm;
layout (location = 2) in vec4 vCol;
layout (location = 3) in vec3 vWPV;
uniform mat3 transform;
out vec2 pos;
out vec2 norm;
out vec4 col;
out vec3 wpv;
void main()
{
	// no transform here, that's done in geoshader
	gl_Position = vec4(vPos, 1.0f, 1.0f);
	pos = vPos;
	norm = vNorm;
	col = vCol;
	wpv = vWPV;
})";

/*
* const int maxInterp (CURRENTLY HARDCODED = 30)
*	maximum number of interpolated vertices between two points on the
*	given path.
* 
* uniform float granularity (CURRENTLY HARDCODED = 0.003)
* 	roughly the distance, in screen units, between adjecent interpolated 
* 	vertices. More precisely, such distance always falls in the interval 
* 	[-50%, +50%) * granularity.
*/
const string geoGLSL = R"(
#version 410
layout (lines) in;
layout (triangle_strip, max_vertices = 64) out;

const int maxInterp = 30;

uniform mat3 transform;
uniform float granularity;
in vec2 norm[];
in vec4 col[];
in vec3 wpv[]; // width, pressure, velocity
out vec4 fCol;

void EmitPoint(vec3 position, vec3 normal, vec4 color)
{
	fCol = color;
	gl_Position = vec4(transform * (position + normal), 1.0f);
	EmitVertex();
	gl_Position = vec4(transform * (position - normal), 1.0f);
	EmitVertex();
}

void main()
{
	//if (wpv[0].y < 0)
	//	EmitTerminus(0);
	
	vec3 p1 = gl_in[0].gl_Position.xyz;
	vec3 p2 = gl_in[1].gl_Position.xyz;

	float dist = distance(p1, p2);
	int n = min(int(round(dist / granularity)), maxInterp);

	EmitPoint(p1, vec3(norm[0], 0) * wpv[0].x, col[0]);
	
	if (n > 1)
	{
		// interpolate
		vec3 t1 = vec3(norm[0].y, -norm[0].x, 0) * dist;
		vec3 t2 = vec3(norm[1].y, -norm[1].x, 0) * dist;
		vec3 f1 = 2*p1+t1-2*p2+t2;
		vec3 f2 = -3*p1+3*p2-2*t1-t2;
		
		for (int i = 1; i < n; i++)
		{
			float x = float(i) / float(n);
			vec3 pi = f1 * x*x*x + f2 * x*x + t1 * x + p1;
			vec3 ti = normalize(3 * f1 * x*x + 2 * f2 * x + t1);
			EmitPoint(pi, vec3(-ti.y, ti.x, 0) * mix(wpv[0].x, wpv[1].x, x), mix(col[0], col[1], x));
		}
	}

	EmitPoint(p2, vec3(norm[1], 0) * wpv[1].x, col[1]);

	EndPrimitive();
	//if (wpv[1].y < 0)
	//	EmitTerminus(1);
})";

const string fragGLSL = R"(
#version 410
in vec4 fCol;
out vec4 fragColor;
void main()
{
	fragColor = fCol;
})";

bool vg::Stroke::RenderDebug = true;

class vg::Stroke::Renderer : public GraphicsRenderer
{
private:
	static Shader* shader;
	Stroke& _s;
	Shape* _shape = nullptr;
	bool _noData = true;

public:
	Renderer(Stroke& s);
	~Renderer();

	GraphicObject& graphicObject() const override { return _s; }
	void render(GraphicsRenderContext& cxt) override;
	void update() override;
};

Shader* vg::Stroke::Renderer::shader = nullptr;

vg::Stroke::Renderer::Renderer(Stroke& s) :
	_s(s)
{
	if (shader == nullptr)
	{
		ShaderPart* v = new ShaderPart(ShaderPart::Type::Vertex, vertGLSL);
		ShaderPart* f = new ShaderPart(ShaderPart::Type::Fragment, fragGLSL);
		ShaderPart* g = new ShaderPart(ShaderPart::Type::Geometry, geoGLSL);
		shader = new Shader{ v, g, f };

		Shared::add({ v, f, g, shader });
	}

	_shape = new Shape(Shape::Type::LineStrip);
	_shape->addDataSlot(11);
	_shape->bindData(0, 0, 2, 11, 0);
	_shape->bindData(0, 1, 2, 11, 2);
	_shape->bindData(0, 2, 4, 11, 4);
	_shape->bindData(0, 3, 3, 11, 8);

	update();
}

vg::Stroke::Renderer::~Renderer()
{
	if (_shape != nullptr)
	{
		_shape->finalize();
		delete _shape;
	}
}

void vg::Stroke::Renderer::update()
{
	_version = _s.version();

	auto size = _s.points.size();
	if (size == 0)
	{
		_noData = true;
		return;
	}
	else _noData = false;

	Point* data = new Point[size];
	copy(_s.points.cbegin(), _s.points.cend(), data);
	data[0].pressure *= -1;
	data[size - 1].pressure *= -1;

	if (_shape->size() != size)
		_shape->getData(0).set(size, (float*)data, Shape::StorageHint::Dynamic);
	else
		_shape->getData(0).setPartial(0, size, (float*)data);

	delete[] data;
}

void vg::Stroke::Renderer::render(GraphicsRenderContext& cxt)
{
	if (_noData) return;

	shader->use();
	shader->set("transform", Context::currentTarget().transformMatrix());
	shader->set("granularity", 0.003f);
	_shape->draw();

	if (vg::Stroke::RenderDebug)
	{
		float s = 0.0015f / Context::currentTarget().transform().zoom.x;
		size_t size = _s.points.size();

		auto prev = _s.points[0];
		Draw::crossMarker(prev.position, s * 2.0f, s, Colors::red);
		for (size_t i = 1; i < size; i++)
		{
			auto pt = _s.points[i];

			//Draw::line(pt.position, pt.position + pt.normal * (s * 20.0f), s, Colors::blue);
			Draw::crossMarker(pt.position, s * 3.0f, s, Colors::paleVioletRed);
		}
	}
}

void vg::Stroke::calcNormal()
{
	int size = points.size();
	if (size > 2)
	{
		for (int i = 1; i < size - 1; i++)
		{
			vec2 p1 = points[i - 1].position;
			vec2 p2 = points[i].position;
			vec2 p3 = points[i + 1].position;
			vec2 tangent = math::normalize(
				math::normalize(p2 - p1) + math::normalize(p3 - p2));

			points[i].normal = math::normal(tangent);
		}
		points[0].normal = points[1].normal;
		points[size - 2].normal = points[size - 1].normal;
	}
	else if (size == 2)
	{
		points[0].normal = points[1].normal =
			math::normalize(math::normal(points[1].position - points[0].position));
	}
}

static float _pointToSeg(invec2 p, invec2 a, invec2 b)
{
	float cross = (b.x - a.x) * (p.x - a.x) + (b.y - a.y) * (p.y - a.y);
	if (cross <= 0) return math::distance(p, a);
	float dist = math::distance(a, b);
	if (cross >= dist) return math::distance(p, b);
	float r = cross / dist;
	vec2 d = a + (b - a) * r;
	return math::distance(p, d);
}

bool vg::Stroke::isInside(invec2 p, float time)
{
	int n = points.size() / 2;
	//float sm = 1000;
	for (int i = 0; i < n; i++)
	{
		float dist = _pointToSeg(p, points[i * 2].position, points[i * 2 + 1].position);
		//sm = min(sm, dist);
		if (dist <= points[i * 2].width)
			return true;
	}
	//cout << sm << endl;
	return false;
}

GraphicsRenderer* vg::Stroke::createRenderer()
{
	return new Renderer(*this);
}

serializer_implementation(vg::Stroke)(Primitive& obj, ostream& out)
{
	auto& s = (Stroke&)obj;
	binaryWrite(s.headStyle, out);
	binaryWrite(s.tailStyle, out);
	binaryWrite(s.enclosed, out);
	binaryWrite(s.points.size(), out);
	for (auto& p : s.points)
	{
		binaryWrite(p.position, out);
		binaryWrite(p.normal, out);
		binaryWrite(p.color, out);
		binaryWrite(p.width, out);
		binaryWrite(p.pressure, out);
		binaryWrite(p.velocity, out);
	}
}

deserializer_implementation(vg::Stroke)(istream& in)
{
	auto s = new Stroke();
	binaryRead(s->headStyle, in);
	binaryRead(s->tailStyle, in);
	binaryRead(s->enclosed, in);
	size_t n;
	binaryRead(n, in);
	for (; n > 0; n--)
	{
		Point p;
		binaryRead(p.position, in);
		binaryRead(p.normal, in);
		binaryRead(p.color, in);
		binaryRead(p.width, in);
		binaryRead(p.pressure, in);
		binaryRead(p.velocity, in);
		s->points.push_back(p);
	}
	return s;
}